package org.commcare.android.logging; import android.util.Log; import org.apache.http.HttpResponse; import org.apache.http.entity.mime.MIME; import org.apache.http.entity.mime.MultipartEntity; import org.apache.http.entity.mime.content.StringBody; import org.commcare.CommCareApp; import org.commcare.CommCareApplication; import org.commcare.dalvik.R; import org.commcare.logging.AndroidLogSerializer; import org.commcare.logging.DeviceReportWriter; import org.commcare.models.database.SqlStorage; import org.commcare.network.HttpRequestGenerator; import org.commcare.preferences.CommCareServerPreferences; import org.javarosa.core.model.User; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.PrintStream; import java.io.UnsupportedEncodingException; import java.nio.charset.IllegalCharsetNameException; import java.nio.charset.UnsupportedCharsetException; import java.util.Date; /** * Catch exceptions that are going to crash the phone, grab the stack trace, * and upload to device logs. * * @author csims@dimagi.com **/ public class ForceCloseLogger { private static final String TAG = ForceCloseLogger.class.getSimpleName(); private static SqlStorage<ForceCloseLogEntry> logStorage; public static void registerStorage(SqlStorage<ForceCloseLogEntry> storage) { logStorage = storage; } public static void reportExceptionInBg(final Throwable exception) { new Thread(new Runnable() { @Override public void run() { sendToServerOrStore(exception); } }).start(); } /** * Attempts to send a force close report for the given exception to the server * immediately. If this fails, instead writes the exception to storage as a ForceCloseLogEntry, * so that we can attempt to send it again later during a normal LogSubmissionTask */ private static void sendToServerOrStore(Throwable exception) { ByteArrayOutputStream streamToWriteErrorTo = new ByteArrayOutputStream(); String exceptionText = getStackTrace(exception); String submissionUri = getSubmissionUri(); ForceCloseLogEntry entry = new ForceCloseLogEntry(exceptionText); try { DeviceReportWriter reportWriter = new DeviceReportWriter(streamToWriteErrorTo); reportWriter.addReportElement(new ForceCloseLogSerializer(entry)); // TEMPORARILY write this in the old format as well, until HQ starts parsing the new one reportWriter.addReportElement(new AndroidLogSerializer<ForceCloseLogEntry>(entry)); reportWriter.write(); if (!sendErrorToServer(streamToWriteErrorTo.toByteArray(), submissionUri)) { writeErrorToStorage(entry); } } catch (IOException e) { e.printStackTrace(); // Couldn't create a report writer, so just manually create the data we want to send String fsDate = new Date().toString(); byte[] data = ("<?xml version='1.0' ?><n0:device_report xmlns:n0=\"http://code.javarosa.org/devicereport\"><device_id>FAILSAFE</device_id><report_date>" + fsDate + "</report_date><log_subreport><log_entry date=\"" + fsDate + "\"><entry_type>forceclose</entry_type><entry_message>" + exceptionText + "</entry_message></log_entry></log_subreport></device_report>").getBytes(); if (!sendErrorToServer(data, submissionUri)) { writeErrorToStorage(entry); } } } /** * Try to send the given data to the given uri * * @return If send was successful */ private static boolean sendErrorToServer(byte[] dataToSend, String submissionUri) { String payload = new String(dataToSend); Log.d(TAG, "Outgoing payload: " + payload); MultipartEntity entity = new MultipartEntity(); try { //Apparently if you don't have a filename in the multipart wrapper, some receivers //don't properly receive this post. StringBody body = new StringBody(payload, "text/xml", MIME.DEFAULT_CHARSET) { @Override public String getFilename() { return "exceptionreport.xml"; } }; entity.addPart("xml_submission_file", body); } catch (IllegalCharsetNameException | UnsupportedEncodingException | UnsupportedCharsetException e1) { e1.printStackTrace(); return false; } HttpRequestGenerator generator; try { User user = CommCareApplication.instance().getSession().getLoggedInUser(); generator = new HttpRequestGenerator(user); } catch (Exception e) { generator = HttpRequestGenerator.buildNoAuthGenerator(); } try { HttpResponse response = generator.postData(submissionUri, entity); ByteArrayOutputStream bos = new ByteArrayOutputStream(); response.getEntity().writeTo(bos); Log.d(TAG, "Response: " + new String(bos.toByteArray())); } catch (IOException e) { e.printStackTrace(); return false; } return true; } private static void writeErrorToStorage(ForceCloseLogEntry entry) { if (logStorage != null) { logStorage.write(entry); } } private static String getSubmissionUri() { CommCareApp currentApp = CommCareApplication.instance().getCurrentApp(); if (currentApp != null) { return currentApp.getAppPreferences().getString( CommCareServerPreferences.PREFS_SUBMISSION_URL_KEY, CommCareApplication.instance().getString(R.string.PostURL)); } else { return CommCareApplication.instance().getString(R.string.PostURL); } } public static String getStackTrace(Throwable e) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); e.printStackTrace(new PrintStream(bos)); return new String(bos.toByteArray()); } public static String getStackTraceWithContext(Throwable e) { String stackTrace = getStackTrace(e); if (e.getCause() != null) { stackTrace += "Sub Context: \n" + getStackTrace(e.getCause()); } return stackTrace; } }